package data.bean;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import common.Logger;
import common.Utils;

import data.exception.InvalidDataBeanException;
import data.exception.InvalidFieldException;

/**
 * The base class for database object bean. Normally one bean corresponds to a
 * database table but not necessarily.
 * 
 * @author chuxiaoyuan
 */
public abstract class DataBean {

	/**
	 * Constructor.
	 * 
	 * @throws InvalidDataBeanException
	 *             Invalid DataBean
	 */
	public DataBean() {

		// Get descriptor.
		DatabaseDescriptor descriptor = DB_DESCRIPTORS.get(this.getClass());
		assert descriptor != null;

		// Set references.
		this.setTableName(descriptor.getTableName());
		this.fields = descriptor.getFields();
		this.primaryKey = descriptor.getPrimaryKey();
		this.autoGeneratedFields = descriptor.getAutoGeneratedFields();
		this.logger = new Logger(this.getClass());
	}

	/**
	 * The logger.
	 */
	private Logger logger;
	
	/**
	 * DataBean database descriptors repository.
	 */
	private static Map<Class<? extends DataBean>, DatabaseDescriptor>	
		DB_DESCRIPTORS	= new HashMap<Class<? extends DataBean>, DatabaseDescriptor>();

	/**
	 * Register bean class.
	 * 
	 * @param clazz
	 *            the runtime Class type.
	 */
	protected static void REGISTER(Class<? extends DataBean> clazz)
			throws ExceptionInInitializerError {

		// Register new entry in the repository.
		DatabaseDescriptor descriptor = new DatabaseDescriptor();
		DB_DESCRIPTORS.put(clazz, descriptor);

		// Get database entity information
		DatabaseEntity entity = clazz.getAnnotation(DatabaseEntity.class);
		if (entity == null) {

			// Not a valid DataBean
			throw new ExceptionInInitializerError(
					new InvalidDataBeanException(
							"Invalid DataBean: need to be annotated with DatabaseEntity!"));
		}
		descriptor.setTableName(entity.tableName());

		// Get database table field information.
		for (Field field : clazz.getDeclaredFields()) {

			// Get database fields
			DatabaseField dbfield = field.getAnnotation(DatabaseField.class);
			if (null != dbfield) {

				// Get a database field
				descriptor.getFields().add(dbfield);

				// A primary key field
				if (dbfield.primaryKey()) {
					descriptor.getPrimaryKey().add(dbfield);
				}
				
				// Auto generated fields
				if (dbfield.autoGenerate()) {
					descriptor.getAutoGeneratedFields().add(dbfield);
				}
			}
		}
	}

	/**
	 * The table name.
	 */
	private String	tableName;

	/**
	 * The database fields.
	 */
	private List<DatabaseField>	 fields;

	/**
	 * Primary key fields. (a primary key can be combined with many fields)
	 */
	private List<DatabaseField>	 primaryKey;
	
	/**
	 * Auto generated fields.
	 */
	private List<DatabaseField>	 autoGeneratedFields;

	/**
	 * Get database fields.
	 */
	public List<DatabaseField> getFields() {
		return this.fields;
	}

	/**
	 * Get fields of the specified names.
	 */
	public List<DatabaseField> getFields(List<String> fieldsName) {

		List<DatabaseField> results = new ArrayList<DatabaseField>();
		for (DatabaseField field : this.fields) {
			
			if (fieldsName.contains(field.name())) {
				results.add(field);
			}
		}
		return results;
	}

	/**
	 * Get field according to the name.
	 * 
	 * @return null if no such database field.
	 */
	public DatabaseField getField(String fieldName) {

		for (DatabaseField field : this.fields) {
			
			if (fieldName.equals(field.name())) {
				return field;
			}
		}
		return null;
	}

	/**
	 * Dynamically get database field value of this bean.
	 */
	@SuppressWarnings("unchecked")
	public <T> T getFieldValue(DatabaseField field)
			throws InvalidFieldException {

		// Get field information
		String methodName = getLocalGetMethod(field);

		// Dynamically invoke the get method
		Class<? extends DataBean> clazz = this.getClass();
		Class<?>[] types = new Class<?>[] {};
		Method method;
		try {

			method = clazz.getDeclaredMethod(methodName, types);
			return (T) method.invoke(this, new Object[] {});

		} catch (NoSuchMethodException e) {
			String msg = "Get method failed: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (SecurityException e) {
			String msg = "Get method failed: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (IllegalAccessException e) {
			String msg = "Get method failed: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (IllegalArgumentException e) {
			String msg = "Method invocation error: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (InvocationTargetException e) {
			String msg = "Method invocation error: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		}
	}

	/**
	 * Dynamically get database field value of this bean by field name.
	 */
	public <T> T getFieldValue(String fieldName) throws InvalidFieldException {

		// Get database field
		DatabaseField field = this.getField(fieldName);
		if (null == field) {
			
			// No such field
			String msg = "Invalid database field: " + fieldName;
			logger.error(msg);
			throw new InvalidFieldException(msg);
		}

		return getFieldValue(field);
	}

	/**
	 * Dynamically set database field value of this bean.
	 */
	public <T> void setFieldValue(DatabaseField field, T value)
			throws InvalidFieldException {

		// Get field information
		String methodName = getLocalSetMethod(field);

		// Dynamically invoke the get method
		Class<? extends DataBean> clazz = this.getClass();
		Class<?> type = field.type();
		Class<?>[] types = new Class<?>[] { type };
		
		// Do cast to convert to compatible type.
		Object param = null;
		try {
			param = type.cast(value);
		
		} catch (ClassCastException e) {

			// Note! the issue is that for MySQL, the auto generated field will
			// return long though the field is integer. So the type can be 
			// different sometimes.
			try {
				// Do number narrow down cast.
				param = Utils.NarrowNumberCast(type, value);
				//logger.debug("Narrow down cast");
				
			} catch (ClassCastException ce) {
				
				String msg = "Incompatible type from " + value.getClass() +
						" to " + type;
				logger.error(msg, ce);
				throw new InvalidFieldException(msg, e);
			}
		}
		
		Method method;
		try {

			// Invoke method
			method = clazz.getDeclaredMethod(methodName, types);
			method.invoke(this, new Object[] { param });

		} catch (NoSuchMethodException e) {
			String msg = "Get method failed: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (SecurityException e) {
			String msg = "Get method failed: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (IllegalAccessException e) {
			String msg = "Method invocation error: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		} catch (IllegalArgumentException e) {
			String msg = "Method invocation error: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);
			
		} catch (InvocationTargetException e) {
			String msg = "Method invocation error: " + methodName;
			logger.error(msg, e);
			throw new InvalidFieldException(msg, e);

		}
	}

	/**
	 * Dynamically set database field value of this bean by field name.
	 */
	public <T> void setFieldValue(String fieldName, T value)
			throws InvalidFieldException {

		// Get database field
		DatabaseField field = this.getField(fieldName);
		if (null == field) {
			
			// No such field
			String msg = "Invalid database field: " + fieldName;
			logger.error(msg);
			throw new InvalidFieldException(msg);
		}

		setFieldValue(field, value);
	}

	/**
	 * Get local name for GET method
	 */
	private String getLocalGetMethod(DatabaseField field) {
		return getLocalMethod(field, "get");
	}

	/**
	 * Get local name for SET method
	 */
	private String getLocalSetMethod(DatabaseField field) {
		return getLocalMethod(field, "set");
	}

	/**
	 * Get local name for method with prefix.
	 */
	private String getLocalMethod(DatabaseField field, String prefix) {

		// Get local name
		String localName = field.localName();
		if (null == localName || "".equals(localName)) {
			// Local name is default the same as name
			localName = field.name();
		}
		assert localName.length() > 0;

		// Transform to get method name
		StringBuilder sb = new StringBuilder(localName);
		int index = 0;
		sb.setCharAt(index, Character.toUpperCase(sb.charAt(index)));
		return prefix + sb.toString();
	}

	/**
	 * Get the primary key.
	 */
	public List<DatabaseField> getPrimaryKey() {
		return this.primaryKey;
	}

	/**
	 * Test if it is a primary key.
	 */
	public boolean isPrimaryKey(List<String> fieldsName) {

		if (fieldsName.size() != this.primaryKey.size()) {
			return false;
		}

		for (String name : fieldsName) {
			// Match each name with pk fields
			boolean match = false;
			for (DatabaseField field : this.primaryKey) {
				if (name.equals(field.name())) {
					match = true;
					break;
				}
			}
			if (!match) {
				// No match key.
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Get auto generated fields.
	 */
	public List<DatabaseField> getAutoGeneratedFields() {
		return this.autoGeneratedFields;
	}
	
	/**
	 * Test if it is auto generated fields.
	 */
	public boolean isAutoGeneratedField(DatabaseField field) {
		
		for (DatabaseField auto : this.getAutoGeneratedFields()) {
			if (field.name().equals(auto.name())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @return the tableName
	 */
	public String getTableName() {
		return tableName;
	}

	/**
	 * Test if this is a table entity.
	 */
	public boolean isTableEntity() {
		return ((null != this.getTableName()) && !("".equals(this
				.getTableName())));
	}

	/**
	 * @param tableName
	 *            the tableName to set
	 */
	protected void setTableName(String tableName) {
		this.tableName = tableName;
	}

}

/**
 * Database descriptor.
 * 
 * @author chuxiaoyuan
 */
class DatabaseDescriptor {
	public DatabaseDescriptor() {
		// Nothing.
	}

	/**
	 * Table name.
	 */
	private String	tableName;

	/**
	 * Database fields.
	 */
	private List<DatabaseField>	 fields		= new ArrayList<DatabaseField>();

	/**
	 * Primary key.
	 */
	private List<DatabaseField>	 primaryKey	= new ArrayList<DatabaseField>();

	/**
	 * Auto generated fields.
	 */
	private List<DatabaseField> autoGeneratedFields = new ArrayList<DatabaseField>();
	
	public String getTableName() {
		return tableName;
	}

	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	public List<DatabaseField> getFields() {
		return fields;
	}

	public List<DatabaseField> getPrimaryKey() {
		return primaryKey;
	}
	
	public List<DatabaseField> getAutoGeneratedFields() {
		return this.autoGeneratedFields;
	}
}
